package com.db4o.odbgen;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Collections;

import com.db4o.internal.odbgen.XmlSchemaParser;
import com.db4o.internal.odbgen.fileutils.FileLister;
import com.db4o.internal.odbgen.plugins.OptionList;
import com.db4o.odbgen.plugins.Log;
import com.db4o.odbgen.plugins.MainGeneratorPlugin;
import com.db4o.odbgen.plugins.Plugin;
import com.db4o.odbgen.plugins.xmlschema.Element;
import com.db4o.odbgen.plugins.xmlschema.XmlSchema;


/**
 * 
 * 
 * This is the root class of OdbGen project.
 * Use it to initialize and start the generator.
 * @author liviug
 *
 */
public class OdbGen {

    private XmlSchema _xmlSchema;
    private Properties _options = new Properties();
    List<Plugin> _pluginStack;
    private MainGeneratorPlugin _mainGenerator;
    private boolean _startCalled = false;

    
    private enum Option implements OptionList{
        /**
         * Sets the list of plugins used by OdbGen.
         * This is a comma sepparated list of fully quallified class names representing plugins.
         * This list must include a {@link MainGeneratorPlugin}.
         */
        PLUGINS("plugins", null),
        
        /**
         * Appends plugins to the already existing ones.
         * It's main usage is to append plugins to the default plugin stack.
         * This is a comma sepparated list of fully quallified class names representing plugins.
         */
        APPENDPLUGINS("appendPlugins", null),
        
        /**
         * The directory where XmlSchema may be found.
         */
        INPUTDIRECTORY("inputDirectory", null),
        
        /**
         * The language used to generate ObjectSchema.
         * Only one language supported this time: "java".
         * Defaults to 'java'
         */
        TARGETLANGUAGE("targetLanguage", TargetLanguage.JAVA.toString());

        private String _defaultValue;
        private String _name;

        Option(String name, String defaultValue){
            this._name = name;
            this._defaultValue = defaultValue;
        }
        
        /**
         * See {@link OptionList} for details.
         */
        @Override
        public String getName() {
            return this._name;
        }
        
        /**
         * See {@link OptionList} for details.
         */
        @Override
        public String getDefaultValue() {
            return this._defaultValue;
        }
    }

    /**
     * 
     * @author liviug
     * Used to sort the plugins based on their dependencies.
     */
    private static class DependencyNode implements Comparable<DependencyNode>{
        public Plugin plugin;
        public List<DependencyNode> dependencies;
        public int rank;
        @Override
        public int compareTo(DependencyNode o) {
            return this.rank - o.rank;
        }
        @Override
        public boolean equals(Object obj) {
        	if(!(obj instanceof DependencyNode)){
        		return false;
        	}
        	else{
        		return this.compareTo((DependencyNode)obj)==0;
        	}
        }
        
        @Override
        public int hashCode() {
        	assert false : "hashCode is not implemented.";
        	return 0;
        }
    }
    
    /**
     * Use this to start Odbgen via command line.
     * @throws OdbgenException
     */
    public static void main(String[] args) throws OdbgenException {
        OdbGen od = new OdbGen();
        Properties options = new Properties();
        
        //Debug: Print arguments
        //String pargs = "";
        //for(String o : args){
        //  pargs+=o+" ";
        //}
        //System.out.println("Options: "+pargs);
        
        for(int i = 0; i<args.length; i++){
            String option = args[i];
            if(option.startsWith("-")){
                if(i>=args.length-1){
                    System.err.println(String.format("Ignored: '%s'.", option));
                }
                else{
                    String value = args[++i];
                    options.setProperty(option.substring(1, option.length()), value);
                }
            }
            else{
                System.err.println(String.format("Ignored: '%s'.", option));
            }
        }
        od.setOptions(options);
        od.start();
    }
    
    /**
     * Returns the specified option if found, otherwise returns the default value.
     */
    private String getOption(Option option) {
        return this.getOptions().getProperty(
                option.getName(),
                option.getDefaultValue()
                );
    }
    
    /**
     * Retuns OdbGen version.
     */
    public String getVersion(){
        return "0.0.1";
    }
    
    
    
    /**
     * Use this to start generating the ObjectSchema.
     * This method may be called only once for each OdbGen instance.
     */
    public void start() throws OdbgenException{
        if(this._startCalled ==true){
            throw new OdbgenException("The start method must be called only once for each OdbGen instance.");
        }
        else{
            this._startCalled = true;
        }
        
        Log.initialize(this._options);

        try {
            //Get the target language.
            TargetLanguage tl = getTargetLanguage();

            this.buildDefaultOptions(tl);
            
            this.validateOptions();
            
            //Build XmlSchema. This need to be done before initializing plugins.
            this.buildXmlSchema();
            
            //Add the plugins according to options.
            this._pluginStack = new ArrayList<Plugin>();
            this.preparePluginStack(tl);
            this.sortPluginStack();
            this.initializePluginStack();

            //Start the generation
            this._mainGenerator.start();

            //Generate XmlSchema usage statistics.
            this.LogXmlSchemaUsageStatistics();
        }
        catch (OdbgenException e) {
            throw e;
        }
        catch(Exception e){
            throw new OdbgenException(e, "An exception occurred during code generation.");
        }
        
    }

    /**
     * Returns the target language according to options.
     */
    private TargetLanguage getTargetLanguage() throws OdbgenException {
        String tlString = this.getOption(OdbGen.Option.TARGETLANGUAGE); 
        TargetLanguage tl;
        try{
            tl = Enum.valueOf(TargetLanguage.class, tlString);
        }
        catch(IllegalArgumentException e){
            throw new OdbgenException(
                    e, 
                    "The language '%s' is not supported.",
                    tlString
                    );
        }
        return tl;
    }


    /**
     * Adds the necessary plugins to the stack according to options.
     */
    private void preparePluginStack(TargetLanguage targetLanguage) throws OdbgenException {
        
        //Get the options representing plugins and additional plugins.
        String pluginsString = this.getOption(OdbGen.Option.PLUGINS); 
        if(pluginsString==null)
            pluginsString="";
        
        String additionalPluginsString = this.getOption(OdbGen.Option.APPENDPLUGINS); 
        
        if(additionalPluginsString==null)
            additionalPluginsString="";
        
        Map<String, Plugin> plugins = new Hashtable<String, Plugin>();
        
        //Add the specified the plugins to the stack.
        for(String pluginClassName:pluginsString.split(",")){
            if(pluginClassName.length()>0)
                constructPluginObject(pluginClassName, plugins, targetLanguage);
        }
        for(String pluginClassName:additionalPluginsString.split(",")){
            if(pluginClassName.length()>0)
                constructPluginObject(pluginClassName, plugins, targetLanguage);
        }
        
        //There should be at least one plugin in the stack.
        if(plugins.isEmpty()){
            throw new OdbgenException(
                    "No plugins loaded in the stack."
                    );
        }
        
        //Search for the main generator plugin in the stack.
        for(Plugin plugin : plugins.values()){
            if (plugin instanceof MainGeneratorPlugin) {
                if(this._mainGenerator!=null){
                    throw new OdbgenException(
                            "One main generator plugin must be loaded in the stack. Currently there are least two: '%s', '%s'",
                            this._mainGenerator.getClass().getName(),
                            plugin.getClass().getName()
                            );
                }
                this._mainGenerator = (MainGeneratorPlugin) plugin;
            }
        }

        //There should be one main generator plugin in the stack.
        if(this._mainGenerator==null){
            throw new OdbgenException(
                    "No main generator plugin loaded in the stack."
                    );
        }
        
        //Add plugins to the list.
        for(Plugin plugin : plugins.values()){
            this._pluginStack.add(plugin);
        }
        
    }
    

    /**
     * Constructs a plugin instance given it's class name and adds it to the list.
     * @param pluginClassName The name of the class representing the plugin.
     * @param plugins The list to add the plugin instance.
     */
    private void constructPluginObject(
            String pluginClassName, 
            Map<String, Plugin> plugins,
            TargetLanguage targetLanguage
            ) throws OdbgenException {
        //Get the class.
        Class<?> pluginClass = null;
        try{
            pluginClass = Class.forName(pluginClassName);
        }
        catch(ClassNotFoundException e){
            throw new OdbgenException(
                    e, 
                    "The plugin '%s' could not be found. It must be included inside class path.",
                    pluginClassName
                    );
        }
        
        //Instantiate the object.
        Object pluginObj;
        try{
            pluginObj = pluginClass.newInstance();
        }
        catch(Exception e){
            throw new OdbgenException(
                    e, 
                    "The plugin '%s' could not be instantiated.",
                    pluginClass.getName()
                    );
            
        }
        
        //Add the plugin to the list.
        if (pluginObj instanceof Plugin) {
            Plugin plugin = (Plugin) pluginObj;
            if(plugins.containsKey(pluginClass.getName())){
                throw new OdbgenException(
                        "The plugin '%s' is already loaded. Each plugin should be loaded only once.",
                        pluginClass.getName()
                        );
            }
            
            if(!plugin.supportsTargetLanguage(targetLanguage)){
                throw new OdbgenException(
                        "The plugin '%s' does not support the target language '%s'.",
                        pluginClass.getName(),
                        targetLanguage.toString()
                        );
            }
            
            plugins.put(pluginClass.getName(), plugin);
        }
        else{
            throw new OdbgenException(
                    "The object '%s' is not a valid plugin.",
                    pluginClass.getName()
                    );
        }
    }

    /**
     * Initializes all plugins currently in the stack.
     */
    private void initializePluginStack() throws OdbgenException {

        //Initialize the main generator.
        this._mainGenerator.initializeMainGenerator(this, this._xmlSchema, this._pluginStack);

        //Initialize all plugins
        for(Plugin plugin : this._pluginStack){
            plugin.initialize(this._mainGenerator);
        }
    }
    
    /**
     * Sorts the plugin stack according to dependencies.
     * 
     * Algorithm:
     * For each plugin, travel it's dependency path and increase a rank associated
     * with each node on the path.
     * Sort plugins based on that counter.
     * @throws OdbgenException 
     */
    private void sortPluginStack() throws OdbgenException {
        //Generate a dependency paths.
        //Depmap is used for quickly finding nodes. The key is the qualified name of the class. 
        Map<String, DependencyNode> depmap = new Hashtable<String, DependencyNode>();
        for(Plugin plugin : this._pluginStack){
            DependencyNode node = new DependencyNode();
            node.dependencies = new ArrayList<DependencyNode>();
            node.plugin = plugin;
            node.rank = 0;
            depmap.put(plugin.getClass().getName(), node);
        }
        for(DependencyNode node : depmap.values()){
            this.buildDependencyList(node, depmap);
        }
        
        //Update ranking.
        for(DependencyNode node : depmap.values()){
            this.updateDependencyRanking(node);
        }

        //Sort using ranking
        List<DependencyNode> sortedList = new ArrayList<DependencyNode>();
        for(DependencyNode node : depmap.values()){
            sortedList.add(node);
        }
        Collections.sort(sortedList);
        
        
        //Update plugin stack.
        //Because the list is sorted ascending (high ranks last) we have to reverse it.
        this._pluginStack.clear();
        for(int i = sortedList.size()-1; i>=0; i--){
            this._pluginStack.add(sortedList.get(i).plugin);
        }
        
    }

    /**
     * Updates the rank of dependencies.
     * @param node All dependencies of this node will be updated.
     */
    private void updateDependencyRanking(DependencyNode node) {
        for(DependencyNode dep : node.dependencies){
            dep.rank++;
            this.updateDependencyRanking(dep);
        }
    }

    /**
     * Constructs a dependency path for the given node
     * @throws OdbgenException 
     */
    private void buildDependencyList(DependencyNode node, Map<String, DependencyNode> depmap) throws OdbgenException {
        for(Class<? extends Plugin> depClass : node.plugin.getPluginDependencies()){
            if(!depmap.containsKey(depClass.getName())){
                throw new OdbgenException(
                        "Plugin '%s' is dependent on '%s' which is missing from the plugin stack.",
                        node.plugin.getClass().getName(),
                        depClass.getName()
                );
            }
            DependencyNode dependency = depmap.get(depClass.getName());
            node.dependencies.add(dependency);
        }
            
    }

    /**
     * Used to generate usage statistics about the XmlSchema.
     * Normally, after a code generation process has successfully completed, all
     * nodes and attributes of the XmlSchema must have been used.
     * If they are not, this method will present warnings with details.
     */
    private void LogXmlSchemaUsageStatistics() {
        //Get statistics and calculate the severity of the log.
        XmlSchema.SchemaUsageStatistics statistics = this._xmlSchema.buildXmlSchemaUsageStatistics();
        Log.Severity severity;
        if(statistics.notUsedElements>0){
            severity = Log.Severity.WARNING;
        }
        else{
            severity = Log.Severity.INFO;
        }
        
        //Output statistics.
        Log.write(severity, "The XmlSchema contains %s element.", statistics.totalElements);
        if(statistics.notUsedElements>0){
            Log.write(severity, "%s elements have been used.", statistics.usedElements);
            Log.write(severity, "%s elements have not been used.", statistics.notUsedElements);
            Log.write(severity, "Follows the list of elements that were not used.");
            for(Element elem : statistics.notUsedElementList){
                Log.write(severity, "%s - %s", elem.getPath(), elem.getFilePath());
            }
        }
        else{
            Log.write(severity, "All elements have been used during processing.");
        }
        
    }
    

    /**
     * This is used to build the XmlSchema for all xml files found inside
     * input directory.
     */
    private void buildXmlSchema() throws IOException, OdbgenException {

        //Get the list of files representing XmlSchema starting with the input directory.
        String baseDir = this.getOption(OdbGen.Option.INPUTDIRECTORY); 
        
        FileLister lister = new FileLister(new File(baseDir), "^.*\\.[xX][mM][lL]$");
        lister.start();
        List<File> files = lister.getFileList();
        
        //Parse the files and obtain the XmlSchma.
        XmlSchemaParser parser = new XmlSchemaParser(files);
        try{
            this._xmlSchema = parser.parseXmlSchema();
        }
        catch(Exception e){
            throw new OdbgenException(
                    e,
                    "An error occurred while parsing XmlSchema found inside '%s'.",
                    this.getOption(Option.INPUTDIRECTORY)
                    );
        }
    }

    
    /**
     * Sets the a new collection of options for OdbGen.
     */
    public void setOptions(Properties options) {
        this._options = options;
    }

    /**
     * Returns the OdbGen options. 
     * Any plugin in the stack has the possibility to consult the
     * list of options.
     * The result may be used to add additional options.
     */
    public Properties getOptions() {
        return _options;
    }

    /**
     * Builds the default set of options depending on target language.
     * Default plugins are added here.
     */
    private void buildDefaultOptions(TargetLanguage targetLanguage) {
        if(targetLanguage==TargetLanguage.JAVA){

            //Add default plugins.
            String plugins = this.getOption(OdbGen.Option.PLUGINS); 
            if(plugins==null){
                StringBuilder pluginlist = new StringBuilder();
                pluginlist.append(com.db4o.odbgen.plugins.java.maingenerator.MainGenerator.class.getName()+",");
                pluginlist.append(com.db4o.odbgen.plugins.java.enhancements.Constructor.class.getName()+",");
                pluginlist.append(com.db4o.odbgen.plugins.java.database.DefaultValue.class.getName()+",");
                pluginlist.append(com.db4o.odbgen.plugins.java.database.Index.class.getName()+",");
                pluginlist.append(com.db4o.odbgen.plugins.java.database.Unique.class.getName()+",");
                pluginlist.append(com.db4o.odbgen.plugins.java.database.Notnull.class.getName()+",");
                pluginlist.append(com.db4o.odbgen.plugins.java.enhancements.ChangeDetector.class.getName()+",");
                this.getOptions().setProperty(OdbGen.Option.PLUGINS.getName(), pluginlist.toString());
            }
        }
    }

    /**
     * Validates received options.
     * @throws OdbgenException 
     */
    private void validateOptions() throws OdbgenException {
        if(this.getOption(OdbGen.Option.INPUTDIRECTORY)==null){
            throw new OdbgenException(
                    "The option '-%s' is not specified.", 
                    OdbGen.Option.INPUTDIRECTORY.getName()
                    );
        }
    }

    

}


